import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*; // for ChangeListener
import java.awt.geom.*;
import java.util.*; // for collections

import java.lang.*;



public class MapPanel extends SimpleMapPanel implements MouseMotionListener,
		Scrollable, ChangeListener, MouseListener {
	static final long serialVersionUID = 32L;
	static final boolean debugging = false;
	static final double RADIUS = 3.0;
	static final double NEARNESS = 1.0;
	static final int DRAG_TOLERANCE = 5;
	float[] dashes10 = { (float) (10.0 / scale), (float) (6.0 / scale) };
	static final int ALL_MODE = 0;
	private boolean roadCheckBox = true;
	private boolean railCheckBox = true;
	private boolean subCheckBox = true;
	private String selectedSuburb = "";
	// different tabs
	static final int SEARCH_MODE = 0;
	static final int COLOUR_MODE = 1;
	private int mode = ALL_MODE, saveMode = mode;
	private int tabmode = SEARCH_MODE;

	/*
	 * declared in SimpleMapPanel: protected MapData data; protected
	 * AffineTransform af, inverse; protected double scale = 10.0; //
	 * pixels/degree (dummy init value) protected int zoomFactor = 1; protected
	 * Rectangle2D.Double outerRect; protected Rectangle2D.Double innerRect; //
	 * approx what is currently visible (includes scrollbars) protected
	 * DecimalFormat df = new DecimalFormat("##0.000");
	 */
	private Rectangle vpRect; // current
	private Rectangle vpRequest; // pending request, null if none
	private JViewport viewport;
	private JLabel status;
	private Cursor savedCursor = new Cursor(Cursor.DEFAULT_CURSOR);

	private Color bg_colour = Color.RED;
	private Region currentRegion = null, selectedRegion = null;
	private Area selectedArea = null;
	private Road selectedRoad = null;
	private Rail selectedRail = null;
	private Point lastMouseDown; // for drag recognition
	private Point lastDragPos; // for dragging viewport
	private boolean dragging = false;
	private boolean zooming = false;

	private MovableTransport car;

	/**
	 * Initiallises the map display, sets up mouse button listeners.
	 * 
	 * @param status
	 *            is place to display mouse position in pixels and lonlat.
	 */
	public MapPanel(JLabel status) {
		super();
		this.status = status;
		setPreferredSize(new Dimension(500, 500)); // <<<<<<
		addMouseListener(this);
		addMouseMotionListener(this);

		addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				if (zooming)
					zoomRealize(e.getPoint());
				else
					lastMouseDown = e.getPoint();
			}

			public void mouseReleased(MouseEvent e) {
				if (dragging)
					setCursor(savedCursor);
				dragging = false;
			}

			public void mouseClicked(MouseEvent e) {
				if (inverse == null)
					return;
				Point2D.Double lonlat = new Point2D.Double();
				inverse.transform(new Point2D.Double(e.getX(), e.getY()),lonlat);

				if (e.getButton() == 1) {
					changeSuburbColour(lonlat);
				} else if (e.getButton() == 3) {
					findClosestRailOrRoad(lonlat);
				}

		
			}
		});
	}
	
	

	public void setRail(Rail r, Point2D.Double lonlat) {
		selectedRail = r;
		String name = "";
		name = createMessageBox(r.getName(), lonlat,"");
		if (!name.equals("")) {
			if (!name.trim().equalsIgnoreCase(r.getName())) {
				r.setName(name);
				data.setChanged(true);
			}
		}
	}

	public void setRoad(Road d, Point2D.Double lonlat) {
		selectedRoad = d;
		String name = "";
		String statistics = "Name : " + d.getName();
		name = createMessageBox(d.getName(), lonlat,statistics);
		if (!name.equals(""))
		{
			if (!name.trim().equalsIgnoreCase(d.getName()))
			{
				d.setName(name);
				data.setChanged(true);
			}
		}
	}

	public void findClosestRailOrRoad(Point2D.Double lonlat) {

		Road d =null;
		if(roadCheckBox)
		{
			d = data.findRoad(lonlat);
			if (d != null) 
			{
				setRoad(d, lonlat);
			}
		}
		
		if(railCheckBox){
			Rail r = data.findRail(lonlat);
			if (r != null) {
				setRail(r, lonlat);
			}
		}
		repaint();
	}

	public void changeSuburbColour(Point2D.Double lonlat) {
		
		if(subCheckBox){
		
		Area a = data.findSuburb(lonlat, NEARNESS / scale);
		if (a != null) 
		{
			this.selectedSuburb = a.getName();
			a.setBGColour(this.bg_colour);
			setSelectedArea(a);
			String name = "";
			name = createMessageBox(a.getName(), lonlat,"");
			if (!name.equals("")) 
			{
				if (!name.trim().equalsIgnoreCase(a.getName()))
				{
					a.setName(name);
					data.setChanged(true);
				}
			}
		}
		
		repaint();
		
		}
	}

	public void setSelectedSuburb(String suburb) {
		this.selectedSuburb = suburb;
		Area a = data.findSuburb(suburb);
		if (a != null) {
			this.selectedArea = a;
		}
		repaint();
	}

	public void setRoadCheckBox(boolean on) {
		roadCheckBox = on;
		repaint();
	}

	public void setRailCheckBox(boolean on) {
		railCheckBox = on;
		repaint();
	}

	public void setSubCheckBox(boolean on) {
		subCheckBox = on;
		repaint();
	}

	public void setBGColour(Color colour) {
		this.bg_colour = colour;
	}

	public int getMode() {
		return mode;
	}

	public void setTabMode(int tabmode) {
		this.tabmode = tabmode;
	}

	public int getTabMode() {
		return tabmode;
	}

	public void mouseMoved(MouseEvent ev) {
		showWhereMouseIs(ev.getX(), ev.getY());
	}

	public void mouseDragged(MouseEvent ev) {
		showWhereMouseIs(ev.getX(), ev.getY());
		if (dragging) 
		{
			int dx = ev.getX() - lastDragPos.x; // typically +-1
			int dy = ev.getY() - lastDragPos.y; // ditto
			Point vpPos = getLocation();
			setLocation(vpPos.x + dx, vpPos.y + dy);
			lastDragPos = ev.getPoint(); // done this small drag
		} 
		else 
		{
			savedCursor = getCursor();
			setCursor(new Cursor(Cursor.HAND_CURSOR));
			dragging = true;
			lastDragPos = ev.getPoint();
		}
	}

	public void drawRail(Graphics2D g2) {
		if (railCheckBox) {
			for (Rail value : data.getRail().values()) {
				g2.setColor(value.getColour());
				g2.draw(value.getDrawPath());
			}
		}
	}

	public void drawRoad(Graphics2D g2) {
		if (roadCheckBox) {
			for (Road value : data.getRoad().values()) {
				g2.setColor(value.getColour());
				g2.setStroke(new BasicStroke(value.getThickness()));
				if (value == selectedRoad) {
					g2.setStroke(new BasicStroke(50));
					g2.setColor(Color.MAGENTA);
				}
				for (Line2D.Double line : value.getSections())
					g2.draw(line);

			}
		}
	}


	public void drawSuburb(Graphics2D g2) {
		if (subCheckBox) {

			for (Area value : data.getArea().values()) {

				Point2D.Double current = null;
				Point2D.Double prev = null;
				Point2D.Double first = null;

				for (Point2D.Double v : value.getPoints()) {
					current = new Point2D.Double(v.x, v.y);
					if (prev != null) {
						if (value.getName().equalsIgnoreCase(selectedSuburb)) {
							selectedArea = value;
							g2.setColor(this.bg_colour);
							g2.fill(data.drawPolygon(value));
						}

						g2.setColor(value.getColour());
						g2.drawLine((int) prev.x, (int) prev.y,
								(int) current.x, (int) current.y);
					} else {

						first = current;
					}
					prev = current;

				}
			}
		}
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		if (data == null)
			return;
		Graphics2D g2 = (Graphics2D) g;
		g2.setColor(Color.BLACK);

		if (selectedArea != null && subCheckBox == true) {
			g2.setPaint(this.bg_colour);
			g2.fill(data.drawPolygon(selectedArea));
			
		}

		drawRail(g2);
		drawRoad(g2);
		drawSuburb(g2);
		drawPath(g2);
		g2.transform(inverse);
		drawAreaNames(g2);
		drawRoadNames(g2);
	}

	public void drawPath(Graphics2D g2) {
		if (car != null) {
			g2.fillOval((int) car.getLocation().getX(), (int) car.getLocation()
					.getY(), 100, 100);

			if (car.getShortestPath().size() > 0) {
				Iterator<Vertex> iter = car.getShortestPath().iterator();
				Path2D.Double drawPath = new Path2D.Double();
				Point2D prev = null;
				while (iter.hasNext()) {
					Point2D pt = iter.next().getPoint2D();

					if (prev != null)
						drawPath.append(new Line2D.Double(pt, prev), true);
					prev = pt;
				}

				g2.setColor(new Color(120, 81, 169));
				g2.setStroke(new BasicStroke(32));
				g2.draw(drawPath);
			}
		}
	}

	/**
	 * draw region names as text. assumes working in pixel coordinates, 1 pixel
	 * lines.
	 */
	private void drawAreaNames(Graphics2D g2) {
		int count = 0;
		if (zoomFactor > 30 && subCheckBox) {
			for (Area value : data.getArea().values()) {
				drawTheSuburbName(g2, value);
			}
		}

	}
	
	private void drawRoadNames(Graphics2D g2) {
		int count = 0;
		if (zoomFactor > 30 && roadCheckBox) {
			for (Road value : data.getRoad().values()) {
				
				drawTheRoadName(g2, value);
			}
		}

	}

	public void drawTheSuburbName(Graphics2D g2, Area value) {

		Polygon pol = data.drawPolygon(value);
		Rectangle rect = pol.getBounds();
		int theHeight = rect.height;
		Point2D.Double pos = new Point2D.Double(rect.getCenterX(),rect.getCenterY());
		Point2D.Double pos2 = new Point2D.Double();
		af.transform(pos, pos2);
		g2.setColor(Color.BLACK);
		g2.setFont(new Font("Arial", Font.PLAIN, zoomFactor / 6));
		g2.drawString(value.getName(), (int) pos2.getX(), (int) pos2.getY());

	}
	
	public void drawTheRoadName(Graphics2D g2, Road value) {

		Rectangle rect = value.getDrawPath().getBounds();
		int theHeight = rect.height;
		Point2D.Double pos = new Point2D.Double(rect.getCenterX(),rect.getCenterY());
		Point2D.Double pos2 = new Point2D.Double();
		af.transform(pos, pos2);
		g2.setColor(Color.BLACK);
		g2.setFont(new Font("Arial", Font.PLAIN, zoomFactor / 12));
		g2.drawString(value.getName(), (int) pos2.getX(), (int) pos2.getY());

	}

	public String createMessageBox(String message, Point2D.Double lonlat, String statistics) {
		String name = JOptionPane.showInputDialog(null, statistics, message);
		if (name == null) {
			return "";
		}
		return name;
	}

	public void setData(Region data) {
		this.data = data;
		outerRect = data.getBounds();
		innerRect = null; // established later
		zoomFactor = 1;
		mode = ALL_MODE;
		vpRect = viewport.getViewRect(); // pixels
		// setPreferredSize(new Dimension(vpRect.width, vpRect.height));
		setPreferredSize(new Dimension(4000, 4000));
		repaint();
	}


	

	/***
	 * 
	 * Added by terry need to fix zoom
	 * 
	 * @param a
	 */

	public void setSelectedArea(Area a) {
		if (selectedArea == a)
			return;
		selectedArea = a;
		repaint();
	}

	public void setSelectedRegion(Region r) {
		if (selectedRegion == r)
			return;
		selectedRegion = r;
		/*
		 * Point2D.Double pos = r.getRefPoint(); Point2D.Double pos2 = new
		 * Point2D.Double(); af.transform(pos, pos2); Rectangle vpReq = new
		 * Rectangle( (int)(pos2.getX() - 10), (int)(pos2.getY() - 10), 20, 20);
		 */
		Rectangle2D.Double bounds = r.getBounds();
		Point2D.Double pos = new Point2D.Double(bounds.getX(), bounds.getY()
				+ bounds.getHeight());
		Point2D.Double pos2 = new Point2D.Double();
		af.transform(pos, pos2);
		Rectangle vpReq = new Rectangle((int) pos2.getX(), (int) pos2.getY(),
				(int) (scale * bounds.getWidth()),
				(int) (scale * bounds.getHeight()));
		scrollRectToVisible(vpReq);
		repaint();
	}

	/**
	 * @param vp
	 *            expected to be the JViewport part of the JScrollPane that
	 *            contains this panel.
	 */
	public void setViewport(JViewport vp) {
		viewport = vp;
	}

	/**
	 * Displays the current mouse position in the 'status' label in both pixels
	 * and degrees. Because of zooming the x,y values can be large.
	 * 
	 * @param x
	 *            the horizontal mouse position within this panel.
	 * @param y
	 *            the vertical mouse position within this panel.
	 */
	private void showWhereMouseIs(int x, int y) {
		Point2D.Double pt = new Point2D.Double(x, y), lonlat = new Point2D.Double();
		
		ConvRegionFileToUTM conv = new ConvRegionFileToUTM();
		
		String result = "" + x + "," + y;
		if (inverse != null) {
			inverse.transform(pt, lonlat);
			result += "  " + conv.converting(lonlat) + " " + format(lonlat);
		}
		status.setText(result);
	}

	private int scCount = 0; // for debugging

	/**
	 * Respond to changes in the JViewport part of JScrollPane that contains
	 * this panel. Changes may be in position or size including zoomIn or
	 * zoomOut. Always fires PropertyChangeEvent for "viewport.Double", which is
	 * innerRect, the actual lon-lat visible.
	 * 
	 * @param e
	 *            the event whose source is checked vs viewport
	 * @see setViewport
	 */
	public void stateChanged(ChangeEvent e) {
		if (e.getSource() != viewport)
			return;
		vpRect = viewport.getViewRect(); // pixels
		
		Dimension ds = getSize();
		// System.out.print("size=" + ds.width + ", " + ds.height);
		Dimension d = getPreferredSize();
	
		if (ds.width != d.width)
			;// do nothing -- size change underway
		else if (vpRequest != null) {
			Rectangle vpReq = vpRequest;
			vpRequest = null; // prevent doing it twice
		
			// aug11 scrollRectToVisible(vpReq); // will recurse
		} else if (inverse != null) {
			// resize and scroll finished, so find vp in lon-lat
			Point2D.Double pt = new Point2D.Double(vpRect.x, vpRect.y
					+ vpRect.height), lonlat1 = new Point2D.Double(), lonlat2 = new Point2D.Double();
			inverse.transform(pt, lonlat1); // bot left
			pt.setLocation(vpRect.x + vpRect.width, vpRect.y);
			inverse.transform(pt, lonlat2); // top right
			Rectangle2D oldRect = innerRect;
			innerRect = new Rectangle2D.Double(lonlat1.getX(), lonlat1.getY(),
					lonlat2.getX() - lonlat1.getX(), lonlat2.getY()
							- lonlat1.getY());
			firePropertyChange("viewport.Double", oldRect, innerRect);
		}
	}

	/** increase scale by factor 2, maintaining centre of viewport */
	public void zoomIn() {
		Dimension d = getPreferredSize();
		zooming = true;
		zoomFactor *= 2;
		setPreferredSize(new Dimension(2 * d.width, 2 * d.height));
		// maintain centre of vp in new pixel coords:
		int xc = 2 * vpRect.x + vpRect.width; // new coords
		int yc = 2 * vpRect.y + vpRect.height;
		vpRequest = new Rectangle(xc - vpRect.width / 2,
				yc - vpRect.height / 2, vpRect.width, vpRect.height);
		revalidate();

		scrollRectToVisible(vpRequest); // aug11
		repaint();
	}

	/**
	 * reduce scale by factor 2, does not move outside outerRect but otherwise
	 * maintaining centre of viewport
	 */
	public void zoomOut() {
		if (zoomFactor == 1)
			return;
		Dimension d = getPreferredSize();
		Rectangle vpRect = viewport.getViewRect(); // pixels

		zoomFactor /= 2;
		setPreferredSize(new Dimension(d.width / 2, d.height / 2));

		// maintain vp in new coordinates but adjust if near edges:
		int xc = vpRect.x + vpRect.width / 2; // old coords
		int yc = vpRect.y + vpRect.height / 2;
		int newW = vpRect.width; // new coords
		int x = (xc - newW) / 2;
		int newH = vpRect.height;
		int y = (yc - newH) / 2;
		// ensure requested viewport (in new coords) stays in
		// requested preferred rectangle:
		if (x < 0)
			x = 0;
		else if (x + newW > d.width / 2)
			x = d.width / 2 - newW;
		if (y < 0)
			y = 0;
		else if (y + newH > d.height / 2)
			y = d.height / 2 - newH;
		setLocation(-x, -y);
		// vpRequest = new Rectangle(x, y, newW, newH);
		revalidate();
		// System.out.println("store scroll request:" + vpRequest);
		repaint();
	}

	/** revert to zoomFactor = 1 */
	public void zoomOne() {
		if (zoomFactor == 1)
			return;
		Dimension d = getPreferredSize();
		Rectangle vpRect = viewport.getViewRect(); // pixels
		

		zoomFactor = 1;
		setPreferredSize(new Dimension(vpRect.width, vpRect.height));
		setLocation(0, 0);
		// vpRequest = new Rectangle(0, 0, vpRect.width, vpRect.height);
		revalidate();
		
		repaint();
	}

	/**
	 * Actually perform the pending zoom operation. (The user has just pressed
	 * the mouse button where the new viewport centre should be.)
	 */
	public void zoomRealize(Point mousePos) {
		Dimension d = getPreferredSize();
		Rectangle vpRect = viewport.getViewRect(); // pixels
		zoomFactor *= 2;
		setPreferredSize(new Dimension(2 * d.width, 2 * d.height));
		// new top left of vp in new pixel coords:
		int xc = 2 * mousePos.x - vpRect.width / 2; // new coords
		int yc = 2 * mousePos.y - vpRect.height / 2;

		setLocation(-xc, -yc); // position of this huge view "within" viewport
		/*
		 * vpRequest = new Rectangle(xc - vpRect.width/2, yc - vpRect.height/2,
		 * vpRect.width, vpRect.height);
		 */
		revalidate();
		zooming = false;
		repaint();

	}

	

	
	/** Implementing Scrollable: prefer to see all */
	public Dimension getPreferredScrollableViewportSize() {
		return getPreferredSize();
	}

	/** Implementing Scrollable: do nothing */
	public boolean getScrollableTracksViewportWidth() {
		return false;
	}

	/** Implementing Scrollable: do nothing */
	public boolean getScrollableTracksViewportHeight() {
		return false;
	}

	/** Implementing Scrollable: unit one pixel */
	public int getScrollableUnitIncrement(Rectangle visibleRect,
			int orientation, int direction) {
		return 1;
	}

	/** Implementing Scrollable: jump by 10 pixels */
	public int getScrollableBlockIncrement(Rectangle visibleRect,
			int orientation, int direction) {
		return 10;
	}

	class ZoomStackItem {
		public static final int ZOOM_IN = 1;
		public static final int ZOOM_OUT = 2;
		int kind, zoomFactor;
		Dimension prefSize;
		Rectangle vpRect;
		Rectangle2D.Double innerRect;

		ZoomStackItem(int kind, int zoomFactor, Dimension prefSize,
				Rectangle vpRect, Rectangle2D.Double innerRect) {
			this.kind = kind;
			this.zoomFactor = zoomFactor;
			this.prefSize = prefSize;
			this.vpRect = vpRect;
			this.innerRect = innerRect;
		}

		public String toString() {
			return "ZoomStackItem [" + (kind == ZOOM_IN ? "IN" : "OUT")
					+ " zoomFactor:" + zoomFactor + " prefSize:("
					+ prefSize.width + "," + prefSize.height + ") vpRect:["
					+ vpRect.x + "," + vpRect.y + "," + vpRect.width + ","
					+ vpRect.height + "]";
		}
	}

	public AffineTransform getTranform() {
		return this.inverse;
	}

	public void mouseClicked(MouseEvent arg0) {
		// TODO Auto-generated method stub
	}

	public void mouseEntered(MouseEvent arg0) {
		// TODO Auto-generated method stub

	}

	public void mouseExited(MouseEvent arg0) {
		// TODO Auto-generated method stub

	}

	public void mousePressed(MouseEvent arg0) {
		// TODO Auto-generated method stub

	}

	public void mouseReleased(MouseEvent arg0) {
		// TODO Auto-generated method stub

	}

	public void addCar() {
		// TODO Auto-generated method stub
		car = new MovableTransport(data.getRoad());
		car.setPanel(this);
		car.start();
	}
}
